
#include <QApplication>
#include <QEvent>
#include <QMouseEvent>
#include <QScrollBar>
#include <QTimer>

#include <QDebug>

#include "DragArea.h"

static const int KDecelerationTime = 500; // Time in milliseconds it takes to brake

CDragArea::CDragArea(QWidget* aParent)
    : QScrollArea(aParent), iSpeed(0), iDragging(false)
{
    iLayout = NULL;
    iContent = NULL;

    setWidgetResizable(true);
    setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
}

CDragArea::~CDragArea()
{
}

void CDragArea::mousePressEvent(QMouseEvent* aEvent)
{
    iTimerId = 0;
    iSpeed = 0;

    iPreviousY = aEvent->y();
    iTimeStamp = QTime::currentTime();
    aEvent->accept();
}

void CDragArea::mouseMoveEvent(QMouseEvent* aEvent)
{
    const int dragged = iPreviousY - aEvent->y();
    if (iDragging)
    {
#ifndef Q_WS_MAEMO_5
        QTime currentTime = QTime::currentTime();
        if (iTimeStamp != currentTime)
            iSpeed = 1000 * dragged / iTimeStamp.msecsTo(currentTime);
        iTimeStamp = currentTime;
        iPreviousY = aEvent->y();
        QScrollBar* scrollBar = verticalScrollBar();
        if (scrollBar)
        {
            int newValue = qBound(scrollBar->minimum(), scrollBar->value() + dragged, scrollBar->maximum());
            scrollBar->setValue(newValue);
        }
#endif
    }
    else if (abs(dragged) > QApplication::startDragDistance())
    {
        iDragging = true;
        mouseMoveEvent(aEvent); // recursive call
        return;
    }
    QScrollArea::mouseMoveEvent(aEvent);
}

void CDragArea::mouseReleaseEvent(QMouseEvent* aEvent)
{
    QScrollArea::mouseReleaseEvent(aEvent);
    if (!iDragging)
        return;

    iDragging = false;
#ifndef Q_WS_MAEMO_5
    const int dragged = iPreviousY - aEvent->y();
    iDecelerationStarted = QTime::currentTime();
    if (iTimeStamp.msecsTo(iDecelerationStarted) > 200)
        iSpeed = 1000 * dragged / iTimeStamp.msecsTo(iDecelerationStarted);
    iTimeStamp = iDecelerationStarted;
    iPreviousY = aEvent->y();
    QScrollBar* scrollBar = verticalScrollBar();
    if (scrollBar)
    {
        int newValue = qBound(scrollBar->minimum(), scrollBar->value() + dragged, scrollBar->maximum());
        scrollBar->setValue(newValue);
    }
    iTimerId = startTimer(50);
#endif
}

void CDragArea::timerEvent(QTimerEvent* /*aEvent*/)
{
#ifndef Q_WS_MAEMO_5
    QTime currentTime = QTime::currentTime();
    if (iTimeStamp == currentTime)
        return;

    if (iDecelerationStarted.msecsTo(currentTime) > KDecelerationTime)
    {
        StopScrolling();
        return;
    }

    const int newSpeed = iSpeed * (KDecelerationTime - iDecelerationStarted.msecsTo(currentTime)) / KDecelerationTime;

    const int averageSpeed = newSpeed + iSpeed / 2;
    iSpeed = newSpeed;

    const int movement = averageSpeed * iTimeStamp.msecsTo(currentTime) / 1000;
    iTimeStamp = currentTime;

    // TODO: Refactor into common helper function
    QScrollBar* scrollBar = verticalScrollBar();
    if (scrollBar)
    {
        const int desiredValue = scrollBar->value() + movement;
        if (desiredValue < scrollBar->minimum() || desiredValue > scrollBar->maximum())
        {
            StopScrolling();
        }
        int newValue = qBound(scrollBar->minimum(), desiredValue, scrollBar->maximum());
        scrollBar->setValue(newValue);
    }

    if (iSpeed == 0 && iTimerId != 0)
    {
        StopScrolling();
    }
#endif
}

bool CDragArea::IsDragging() const
{
    return iDragging;
}

void CDragArea::addWidget(QWidget* aWidget)
{
    if (!iLayout)
        InitLayout();
    iLayout->addWidget(aWidget);
}

void CDragArea::removeWidget(QWidget* aWidget)
{
    if (iLayout)
        iLayout->removeWidget(aWidget);
}

void CDragArea::setWidget(QWidget* aWidget)
{
    InitLayout();
    addWidget(aWidget);
}

QLayoutItem* CDragArea::itemAt(int aIndex) const
{
    if (iLayout && iLayout->count() > aIndex)
        return iLayout->itemAt(aIndex);
    return NULL;
}

void CDragArea::InitLayout()
{
    delete iContent;
    iContent = NULL;
    iContent = new QFrame(this);
    iContent->setObjectName("iContent");

    // No need to delete -- deleted when iContent is deleted
    iLayout = new QVBoxLayout(this);

    iLayout->setSpacing(0);
    iLayout->setContentsMargins(0, 0, 0, 0);
    iContent->setLayout(iLayout);
    QScrollArea::setWidget(iContent);
}

void CDragArea::AddStretch()
{
    if (iLayout)
        iLayout->addStretch();
}

bool CDragArea::focusNextPrevChild(bool next)
{
    // We have to override the implementation of QScrollArea
    // to get rid of call to ensureWidgetVisible.
    return QWidget::focusNextPrevChild(next);
}

bool CDragArea::eventFilter(QObject *o, QEvent *e)
{
    if (!isActiveWindow()) {
        // If this is not active window, we shouldn't process events here.
        // If this is not checked, the scrollarea can be scrolled with
        // keyboard events.
        return false;
    }

    if (e->type() == QEvent::FocusIn) {
        // Makes sure that the scrollable widget does not re-position
        // itself within the scrollarea -- that should be handled by
        // out custom HandleScrollEvent.
        return false;
    }

    QWidget *focusedWidget = QApplication::focusWidget();
    if (focusedWidget &&
        (iScrollDisableWidgets.contains(focusedWidget) ||
         QApplication::focusWidget()->objectName() == QLatin1String("qt_calendar_calendarview")))
    {
        // Don't use WPScrollArea scrolling for widgets that have opted out.
        // Additionally, scrolling should not happen if QCalendarView is active, but
        // currently there hasn't been found a way to access the widget.
        // Additionally, don't use QScrollArea::eventFilter, because that will
        // break the custom scrolling.
        // TODO how to register QCalendarView to iSrollDisableWidgets?
        return QWidget::eventFilter(o, e);
    }

    if (e->type() == QEvent::KeyPress)
    {
        QKeyEvent* event = static_cast<QKeyEvent*>(e);
        if (event->key() != Qt::Key_Down && event->key() != Qt::Key_Up)
            return QScrollArea::eventFilter(o, e);

        event->ignore();
        HandleScrollEvent(event);
        if (event->isAccepted())
            return true;
    }
    return QScrollArea::eventFilter(o, e);
}

void CDragArea::HandleScrollEvent(QKeyEvent* aEvent)
{
    QWidget* current = widget()->focusWidget();
    QWidget* viewPort = viewport();

    if (aEvent->key() == Qt::Key_Down)
    {
        if (!current && hasFocus() && iLayout)
        {
            // WPScrollArea has focus, so we'll change the
            // focus to the first element within the area.
            // TODO can there be an item that is not a widget?
            QLayoutItem* layoutItem = itemAt(0);
            if (layoutItem && layoutItem->widget())
            {
                current = layoutItem->widget();
            }
        }

        if (current && viewPort)
        {
            aEvent->accept();

            const int currentTop = current->mapTo(widget(), QPoint(0,0)).y();
            const int currentBottom = currentTop + current->size().height();
            const int screenHeight = viewPort->size().height();
            const int screenTop = -widget()->pos().y();
            const int screenBottom = screenTop + screenHeight;

            // Scroll events for the current widget (in-widget scrolling) can take place
            // only if the widget still has focus.
            if (current->hasFocus())
            {
                if (currentTop > screenBottom || currentBottom < screenTop) // User has scrolled off the screen
                {
                    verticalScrollBar()->setValue(verticalScrollBar()->value() + currentTop - screenTop);
                    return;
                }

                const int delta = currentBottom - screenBottom;
                if (delta > screenHeight)
                {
                    verticalScrollBar()->setValue(verticalScrollBar()->value() + screenHeight - 10);
                    return;
                }
                else if (delta > 0)
                {
                    verticalScrollBar()->setValue(verticalScrollBar()->value() + delta);
                    return;
                }
            }

            // Either the widget didn't have focus, or there was no need for in-widget scrolling.
            // Thus, select the next widget.
            focusNextPrevChild(true);
            QWidget* newFocus = widget()->focusWidget();

            const int nextBottom = newFocus->mapTo(widget(), QPoint(0,0)).y() + newFocus->size().height();
            const int nextTop = nextBottom - newFocus->size().height();
            const int desiredDelta = nextBottom - screenBottom;

            if (desiredDelta > screenHeight)
            {
                if (currentBottom > screenTop) // Let's first scroll so that the top is at the top of the screen
                    verticalScrollBar()->setValue(verticalScrollBar()->value() + nextTop - screenTop);
                else
                    verticalScrollBar()->setValue(verticalScrollBar()->value() + screenHeight - 10);
            }
            else if (desiredDelta > 0)
                verticalScrollBar()->setValue(verticalScrollBar()->value() + desiredDelta);
            else if (nextBottom < currentBottom) // Focus has wrapped around
            {
                const int change = nextTop - screenTop;
                if (change < 0)
                    verticalScrollBar()->setValue(verticalScrollBar()->value() + change);
            }
        }
    }
    // TODO: Unify the both cases
    else if (aEvent->key() == Qt::Key_Up)
    {
        if (!current && hasFocus() && iLayout)
        {
            // Focus to the last item in the scrollarea
            // TODO case where the last item is not a widget?
            QLayoutItem* layoutItem = itemAt(iLayout->count() - 1);
            if (layoutItem && layoutItem->widget())
            {
                current = layoutItem->widget();
            }
        }
        if (current && viewPort)
        {
            aEvent->accept();

            const int currentTop = current->mapTo(widget(), QPoint(0,0)).y();
            const int currentBottom = currentTop + current->size().height();
            const int screenHeight = viewPort->size().height();
            const int screenTop = -widget()->pos().y();
            const int screenBottom = screenTop + screenHeight;

            // Same as in the scroll-down-case: check if we need in-widget scrolling
            if (current->hasFocus())
            {
                if (currentTop > screenBottom || currentBottom < screenTop) // User has scrolled off the screen
                {
                    verticalScrollBar()->setValue(verticalScrollBar()->value() + currentTop - screenTop);
                    return;
                }

                const int delta = screenTop - currentTop;
                if (current->hasFocus() && delta > screenHeight)
                {
                    verticalScrollBar()->setValue(verticalScrollBar()->value() - screenHeight + 10);
                    return;
                }
                else if (current->hasFocus() && delta > 0)
                {
                    verticalScrollBar()->setValue(verticalScrollBar()->value() - delta);
                    return;
                }
            }

            focusNextPrevChild(false);
            QWidget* newFocus = widget()->focusWidget();

            const int nextTop = newFocus->mapTo(widget(), QPoint(0,0)).y();
            //const int nextBottom = nextTop - newFocus->size().height();
            const int desiredDelta = screenTop - nextTop;

            if (desiredDelta > screenHeight)
            {
                if (currentTop < screenBottom)
                    verticalScrollBar()->setValue(verticalScrollBar()->value() + currentTop - screenBottom);
                else
                    verticalScrollBar()->setValue(verticalScrollBar()->value() - screenHeight + 10);
            }
            else if (desiredDelta > 0)
                verticalScrollBar()->setValue(verticalScrollBar()->value() - desiredDelta);
            else if (nextTop > currentTop) // Focus has wrapped around
            {
                const int change = (nextTop + newFocus->size().height()) - screenBottom;
                if (change > 0)
                    verticalScrollBar()->setValue(verticalScrollBar()->value() + change);
            }
        }
    }
}

QSize CDragArea::GetVisibleContentSize() const
{
    QSize size = QScrollArea::size();
    // Remove from size the scrollbar width and used margin of 4 pixels
    size.setWidth(size.width() - verticalScrollBar()->geometry().size().width() - 4);
    return size;
}

void CDragArea::ScrollToTop()
{
    verticalScrollBar()->setValue(verticalScrollBar()->minimum());
}

QPoint CDragArea::TopRightInGlobal() const
{
    return mapToGlobal(viewport()->geometry().topRight());
}

void CDragArea::DisableScrollAtWidget(QWidget* aWidget)
{
    iScrollDisableWidgets.append(aWidget);
}

void CDragArea::RemoveSelfScrollingWidget(QWidget* aWidget)
{
    iScrollDisableWidgets.removeAll(aWidget);
}

#ifndef Q_WS_MAEMO_5
void CDragArea::StopScrolling()
{
    if (iTimerId != 0)
        killTimer(iTimerId);
    iTimerId = 0;
    iSpeed = 0;
    emit ScrollingStopped();
}
#endif
